package org.boz.dial;

public class LispCons extends LispObject {
	private LispObject m_head;
	private LispObject m_tail;

	public LispCons(LispObject head, LispObject tail) {
		m_head = head;
		m_tail = tail;
	}

	public boolean equals(Object o) {
		if (o instanceof LispCons) {
			LispCons cons = (LispCons) o;
			return car().equals(cons.car()) && cdr().equals(cons.cdr());
		}
		return false;
	}

	public LispObject car() {
		return m_head;
	}

	public LispObject cdr() {
		return m_tail;
	}

	public LispObject setcdr(LispObject o) {
		m_tail = o;
		return o;
	}

	public String toString() {
		String rval = "(" + m_head;
		if (m_tail != null)
			rval += "." + m_tail;
		return rval + ")";
	}

	public LispObject eval_list(LispObject exps, LispObject env) {
		if (exps == LispPrimitives.nil)
			return LispPrimitives.nil;
		return new LispCons(exps.car().eval(env), eval_list(exps.cdr(), env));
	}

	public LispObject eval(LispObject env) {
		LispObject head = m_head;
		LispObject tail = m_tail;
		while (true) {
			if (head == LispPrimitives.LispIf) {
				LispObject ifcase = tail.cdr().car();
				LispObject elsecase = tail.cdr().cdr();
				if (tail.car().eval(env) != LispPrimitives.nil) {
					return ifcase.eval(env);
				} else {
					LispObject stmt = elsecase;
					for(; stmt.cdr() != LispPrimitives.nil; stmt = stmt.cdr()) {
						stmt.car().eval(env);
					}
					if (stmt == LispPrimitives.nil)
						return stmt;
					
					if (stmt.car() instanceof LispCons) {
						// Tail call elimination
						head = stmt.car().car();
						tail = stmt.car().cdr();
					} else {
						return stmt.car().eval(env);
					}
				}
			} else if (head == LispPrimitives.LispLambda) {
				return new LispProcedure(tail.car(), tail.cdr(), env);
			} else if (head == LispPrimitives.LispQuote) {
				return tail.car();
			} else if (head == LispPrimitives.LispSet) {
				LispObject pair = LispSym.assoc(tail.car(), env);
				LispObject newval = tail.cdr().car().eval(env);
				if (pair == LispPrimitives.nil)
					throw new RuntimeException("unbound symbol '" + tail.car()
							+ "' in set");
				((LispCons) pair).setcdr(newval);
				return newval;
			} else if (head == LispPrimitives.LispDefine) {
				return LispInterpreter.extend_top(tail.car(), tail.cdr().car()
						.eval(env));
			} else if (head == LispPrimitives.LispLet) {
				LispObject bindings = tail.car();
				LispObject body = tail.cdr();
				LispObject letenv = env;
				// Bind symbols to values, creating new env
				for (; bindings != LispPrimitives.nil; bindings = bindings
						.cdr()) {
					LispObject pair = bindings.car();
					LispObject sym = pair.car();
					LispObject val = pair.cdr().car().eval(letenv);
					letenv = new LispCons(new LispCons(sym, val), letenv);
				}
				// Evaluate body
				LispObject current;
				for (current = body; current.cdr() != LispPrimitives.nil; current = current
						.cdr()) {
					current.car().eval(letenv);
				}
				return current.car().eval(letenv);
				// Pop environment
			} else if (head == LispPrimitives.LispProgn) {
				LispObject current;
				for (current = tail; current.cdr() != LispPrimitives.nil; current = current
						.cdr()) {
					current.car().eval(env);
				}
				return current.car().eval(env);
			} else if (head == LispPrimitives.LispDefMacro) {
				return LispPrimitives.nil;
			} else {
				// Function call
				LispObject procedure = head.eval(env);
				LispObject result = procedure.apply(eval_list(tail, env), env);
				// Tail call elimination
				if (result instanceof LispLast) {
					LispLast last = (LispLast) result;
					head = last.getCode().car();
					tail = last.getCode().cdr();
					env = last.getEnv();
				} else {
					return result;
				}
			}
		}
	}
}
